// Copyright (C) 2023 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuit.codegen

import com.google.common.truth.Truth.assertThat
import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.KotlinCompilation.ExitCode
import com.tschuchort.compiletesting.SourceFile
import com.tschuchort.compiletesting.SourceFile.Companion.kotlin
import com.tschuchort.compiletesting.configureKsp
import com.tschuchort.compiletesting.kspProcessorOptions
import com.tschuchort.compiletesting.kspSourcesDir
import java.io.File
import org.intellij.lang.annotations.Language
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
import org.junit.Test

@Suppress("LargeClass", "RedundantVisibilityModifier")
@OptIn(ExperimentalCompilerApi::class)
class CircuitSymbolProcessorTest {
  private val appScope =
    kotlin(
      "AppScope.kt",
      """
      package test

      annotation class AppScope
      """
        .trimIndent(),
    )
  private val anvilAnnotations =
    kotlin(
      "AnvilAnnotations.kt",
      """
      package com.squareup.anvil.annotations
      import kotlin.reflect.KClass

      annotation class ContributesBinding(val scope: KClass<*>)
      annotation class ContributesMultibinding(val scope: KClass<*>)
      """
        .trimIndent(),
    )
  private val circuitSymbols =
    listOf(
      kotlin(
        "Presenter.kt",
        """
        package com.slack.circuit.runtime.presenter
        import com.slack.circuit.runtime.CircuitUiState
        import androidx.compose.runtime.Composable

        public inline fun <UiState : CircuitUiState> presenterOf(
          crossinline body: @Composable () -> UiState
        ): Presenter<UiState> {
          throw NotImplementedError()
        }
        """
          .trimIndent(),
      ),
      kotlin(
        "Ui.kt",
        """
        package com.slack.circuit.runtime.ui
        import com.slack.circuit.runtime.CircuitUiState
        import com.slack.circuit.runtime.ui.Ui
        import androidx.compose.runtime.Composable
        import androidx.compose.ui.Modifier

        public inline fun <UiState : CircuitUiState> ui(
          crossinline body: @Composable (state: UiState, modifier: Modifier) -> Unit
        ): Ui<UiState> {
          throw NotImplementedError()
        }
        """
          .trimIndent(),
      ),
    )
  private val hiltSymbols =
    listOf(
      kotlin(
        "SingletonComponent.kt",
        """
        package dagger.hilt.components

        annotation class SingletonComponent
        """
          .trimIndent(),
      ),
      kotlin(
        "HiltAnnotations.kt",
        """
        package dagger.hilt
        import kotlin.reflect.KClass

        annotation class InstallIn(val value: KClass<*>)
        """
          .trimIndent(),
      ),
      kotlin(
        "Origins.kt",
        """
        package dagger.hilt.codegen
        import kotlin.reflect.KClass

        annotation class OriginatingElement(val topLevelClass: KClass<*>)
        """
          .trimIndent(),
      ),
    )
  private val kotlinInjectSymbols =
    listOf(
      kotlin(
        "KIAnnotations.kt",
        """
        package me.tatarka.inject.annotations

        annotation class Assisted
        annotation class Inject
        """
          .trimIndent(),
      ),
      kotlin(
        "AmazonAnnotations.kt",
        """
        package software.amazon.lastmile.kotlin.inject.anvil
        import kotlin.reflect.KClass

        annotation class ContributesBinding(val scope: KClass<*>, val multibinding: Boolean = false)
        """
          .trimIndent(),
      ),
      kotlin(
        "AmazonAnnotationsInternal.kt",
        """
        package software.amazon.lastmile.kotlin.inject.anvil.internal
        import kotlin.reflect.KClass

        annotation class Origin(val value: KClass<*>)
        """
          .trimIndent(),
      ),
    )
  private val metroSymbols =
    kotlin(
      "Inject.kt",
      """
      package dev.zacsweers.metro
      import kotlin.reflect.KClass

      abstract class AppScope
      annotation class Inject
      annotation class Assisted(val value: String = "")
      annotation class AssistedFactory
      annotation class Origin(val value: KClass<*>)
      annotation class ContributesIntoSet(val scope: KClass<*>)
      interface Provider<T> {
        operator fun invoke(): T
      }
      """
        .trimIndent(),
    )
  private val screens =
    kotlin(
      "Screens.kt",
      """
      package test

      import com.slack.circuit.runtime.CircuitUiState
      import com.slack.circuit.runtime.screen.Screen

      object HomeScreen : Screen {
        data class State(val message: String) : CircuitUiState
      }
      data class FavoritesScreen(val userId: String) : Screen {
        data class State(val count: Int) : CircuitUiState
      }
      """
        .trimIndent(),
    )

  @Test
  fun simpleUiFunction_withObjectScreen_noState() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestUi.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier

          @CircuitInject(HomeScreen::class, AppScope::class)
          @Composable
          fun Home(modifier: Modifier = Modifier) {

          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/HomeFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.CircuitUiState
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.slack.circuit.runtime.ui.ui
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class HomeFactory @Inject constructor() : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            HomeScreen -> ui<CircuitUiState> { _, modifier -> Home(modifier = modifier) }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun simpleUiFunction_withClassScreen_noState() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestUi.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          @Composable
          fun Favorites(modifier: Modifier = Modifier) {

          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.CircuitUiState
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.slack.circuit.runtime.ui.ui
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class FavoritesFactory @Inject constructor() : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            is FavoritesScreen -> ui<CircuitUiState> { _, modifier -> Favorites(modifier = modifier) }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun simpleUiFunction_withInjectedClassScreen_noState() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestUi.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          @Composable
          fun Favorites(screen: FavoritesScreen, modifier: Modifier = Modifier) {

          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.CircuitUiState
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.slack.circuit.runtime.ui.ui
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class FavoritesFactory @Inject constructor() : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            is FavoritesScreen -> ui<CircuitUiState> { _, modifier -> Favorites(modifier = modifier, screen = screen) }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun simpleUiFunction_withInjectedObjectScreen_noState() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestUi.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier

          @CircuitInject(HomeScreen::class, AppScope::class)
          @Composable
          fun Home(screen: HomeScreen, modifier: Modifier = Modifier) {
            throw NotImplementedError()
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/HomeFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.CircuitUiState
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.slack.circuit.runtime.ui.ui
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class HomeFactory @Inject constructor() : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            HomeScreen -> ui<CircuitUiState> { _, modifier -> Home(modifier = modifier, screen = screen as HomeScreen) }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun simpleUiFunction_withObjectScreen_withState() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestUi.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier

          @CircuitInject(HomeScreen::class, AppScope::class)
          @Composable
          fun Home(state: HomeScreen.State, modifier: Modifier = Modifier) {

          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/HomeFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.slack.circuit.runtime.ui.ui
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class HomeFactory @Inject constructor() : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            HomeScreen -> ui<HomeScreen.State> { state, modifier -> Home(state = state, modifier = modifier) }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun simpleUiFunction_withClassScreen_withState() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestUi.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          @Composable
          fun Favorites(state: FavoritesScreen.State, modifier: Modifier = Modifier) {

          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.slack.circuit.runtime.ui.ui
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class FavoritesFactory @Inject constructor() : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            is FavoritesScreen -> ui<FavoritesScreen.State> { state, modifier -> Favorites(state = state, modifier = modifier) }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun simpleUiFunction_withInjectedClassScreen_withState() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestUi.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          @Composable
          fun Favorites(state: FavoritesScreen.State, screen: FavoritesScreen, modifier: Modifier = Modifier) {

          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.slack.circuit.runtime.ui.ui
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class FavoritesFactory @Inject constructor() : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            is FavoritesScreen -> ui<FavoritesScreen.State> { state, modifier -> Favorites(state = state, modifier = modifier, screen = screen) }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun simpleUiFunction_withInjectedObjectScreen_withState() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestUi.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier

          @CircuitInject(HomeScreen::class, AppScope::class)
          @Composable
          fun Home(state: HomeScreen.State, screen: HomeScreen, modifier: Modifier = Modifier) {

          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/HomeFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.slack.circuit.runtime.ui.ui
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class HomeFactory @Inject constructor() : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            HomeScreen -> ui<HomeScreen.State> { state, modifier -> Home(state = state, modifier = modifier, screen = screen as HomeScreen) }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun uiClass_noInjection() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestUi.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.ui.Ui
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          class Favorites : Ui<FavoritesScreen.State> {
            @Composable
            override fun Content(state: FavoritesScreen.State, modifier: Modifier) {

            }
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class FavoritesFactory @Inject constructor() : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            is FavoritesScreen -> Favorites()
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun uiClass_simpleInjection() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestUi.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.ui.Ui
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier
          import jakarta.inject.Inject

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          class Favorites @Inject constructor() : Ui<FavoritesScreen.State> {
            @Composable
            override fun Content(state: FavoritesScreen.State, modifier: Modifier) {

            }
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject
        import jakarta.inject.Provider

        @ContributesMultibinding(AppScope::class)
        public class FavoritesFactory @Inject constructor(
          private val provider: Provider<Favorites>,
        ) : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            is FavoritesScreen -> provider.get()
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun specifying_javax_uses_javax() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestUi.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.ui.Ui
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier
          import javax.inject.Inject

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          class Favorites @Inject constructor() : Ui<FavoritesScreen.State> {
            @Composable
            override fun Content(state: FavoritesScreen.State, modifier: Modifier) {

            }
          }
          """
            .trimIndent(),
        ),
      kspOptions = mapOf(CircuitOptions.USE_JAVAX_ONLY to "true"),
      generatedFilePath = "test/FavoritesFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.squareup.anvil.annotations.ContributesMultibinding
        import javax.inject.Inject
        import javax.inject.Provider

        @ContributesMultibinding(AppScope::class)
        public class FavoritesFactory @Inject constructor(
          private val provider: Provider<Favorites>,
        ) : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            is FavoritesScreen -> provider.get()
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun uiClass_simpleInjection_secondaryConstructor() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestUi.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.ui.Ui
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier
          import jakarta.inject.Inject

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          class Favorites(val value: String) : Ui<FavoritesScreen.State> {

            @Inject constructor() : this("injected")

            @Composable
            override fun Content(state: FavoritesScreen.State, modifier: Modifier) {

            }
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject
        import jakarta.inject.Provider

        @ContributesMultibinding(AppScope::class)
        public class FavoritesFactory @Inject constructor(
          private val provider: Provider<Favorites>,
        ) : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            is FavoritesScreen -> provider.get()
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun uiClass_assistedInjection() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestUi.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.CircuitContext
          import com.slack.circuit.runtime.ui.Ui
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier
          import dagger.assisted.Assisted
          import dagger.assisted.AssistedFactory
          import dagger.assisted.AssistedInject

          class Favorites @AssistedInject constructor(
            @Assisted private val screen: FavoritesScreen,
            @Assisted private val circuitContext: CircuitContext,
          ) : Ui<FavoritesScreen.State> {
            @CircuitInject(FavoritesScreen::class, AppScope::class)
            @AssistedFactory
            fun interface Factory {
              fun create(screen: FavoritesScreen, circuitContext: CircuitContext): Favorites
            }

            @Composable
            override fun Content(state: FavoritesScreen.State, modifier: Modifier) {

            }
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class FavoritesFactory @Inject constructor(
          private val factory: Favorites.Factory,
        ) : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            is FavoritesScreen -> factory.create(screen = screen, circuitContext = context)
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun simplePresenterFunction_withObjectScreen() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestPresenter.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable

          @CircuitInject(HomeScreen::class, AppScope::class)
          @Composable
          fun HomePresenter(): HomeScreen.State {
            throw NotImplementedError()
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/HomePresenterFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.Navigator
        import com.slack.circuit.runtime.presenter.Presenter
        import com.slack.circuit.runtime.presenter.presenterOf
        import com.slack.circuit.runtime.screen.Screen
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class HomePresenterFactory @Inject constructor() : Presenter.Factory {
          override fun create(
            screen: Screen,
            navigator: Navigator,
            context: CircuitContext,
          ): Presenter<*>? = when (screen) {
            HomeScreen -> presenterOf { HomePresenter() }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun simplePresenterFunction_withClassScreen() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestPresenter.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          @Composable
          fun FavoritesPresenter(): FavoritesScreen.State {
            throw NotImplementedError()
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesPresenterFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.Navigator
        import com.slack.circuit.runtime.presenter.Presenter
        import com.slack.circuit.runtime.presenter.presenterOf
        import com.slack.circuit.runtime.screen.Screen
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class FavoritesPresenterFactory @Inject constructor() : Presenter.Factory {
          override fun create(
            screen: Screen,
            navigator: Navigator,
            context: CircuitContext,
          ): Presenter<*>? = when (screen) {
            is FavoritesScreen -> presenterOf { FavoritesPresenter() }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun simplePresenterFunction_withInjectedClassScreen() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestPresenter.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.Navigator
          import androidx.compose.runtime.Composable

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          @Composable
          fun FavoritesPresenter(screen: FavoritesScreen, navigator: Navigator): FavoritesScreen.State {
            throw NotImplementedError()
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesPresenterFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.Navigator
        import com.slack.circuit.runtime.presenter.Presenter
        import com.slack.circuit.runtime.presenter.presenterOf
        import com.slack.circuit.runtime.screen.Screen
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class FavoritesPresenterFactory @Inject constructor() : Presenter.Factory {
          override fun create(
            screen: Screen,
            navigator: Navigator,
            context: CircuitContext,
          ): Presenter<*>? = when (screen) {
            is FavoritesScreen -> presenterOf { FavoritesPresenter(screen = screen, navigator = navigator) }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun simplePresenterFunction_withInjectedClassScreen_kotlinInjectAnvil() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestPresenter.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.Navigator
          import androidx.compose.runtime.Composable

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          @Composable
          fun FavoritesPresenter(screen: FavoritesScreen, navigator: Navigator): FavoritesScreen.State {
            throw NotImplementedError()
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesPresenterFactory.kt",
      codegenMode = CodegenMode.KOTLIN_INJECT_ANVIL,
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.Navigator
        import com.slack.circuit.runtime.presenter.Presenter
        import com.slack.circuit.runtime.presenter.presenterOf
        import com.slack.circuit.runtime.screen.Screen
        import me.tatarka.inject.annotations.Inject
        import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding

        @Inject
        @ContributesBinding(
          AppScope::class,
          multibinding = true,
        )
        public class FavoritesPresenterFactory : Presenter.Factory {
          override fun create(
            screen: Screen,
            navigator: Navigator,
            context: CircuitContext,
          ): Presenter<*>? = when (screen) {
            is FavoritesScreen -> presenterOf { FavoritesPresenter(screen = screen, navigator = navigator) }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun simplePresenterFunction_withInjectedObjectScreen() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestPresenter.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.Navigator
          import androidx.compose.runtime.Composable

          @CircuitInject(HomeScreen::class, AppScope::class)
          @Composable
          fun HomePresenter(screen: HomeScreen, navigator: Navigator): HomeScreen.State {
            throw NotImplementedError()
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/HomePresenterFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.Navigator
        import com.slack.circuit.runtime.presenter.Presenter
        import com.slack.circuit.runtime.presenter.presenterOf
        import com.slack.circuit.runtime.screen.Screen
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class HomePresenterFactory @Inject constructor() : Presenter.Factory {
          override fun create(
            screen: Screen,
            navigator: Navigator,
            context: CircuitContext,
          ): Presenter<*>? = when (screen) {
            HomeScreen -> presenterOf { HomePresenter(screen = screen as HomeScreen, navigator = navigator) }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun presenterClass_noInjection() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestPresenter.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.presenter.Presenter
          import androidx.compose.runtime.Composable

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          class FavoritesPresenter : Presenter<FavoritesScreen.State> {
            @Composable
            override fun present(): FavoritesScreen.State {
              throw NotImplementedError()
            }
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesPresenterFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.Navigator
        import com.slack.circuit.runtime.presenter.Presenter
        import com.slack.circuit.runtime.screen.Screen
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class FavoritesPresenterFactory @Inject constructor() : Presenter.Factory {
          override fun create(
            screen: Screen,
            navigator: Navigator,
            context: CircuitContext,
          ): Presenter<*>? = when (screen) {
            is FavoritesScreen -> FavoritesPresenter()
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun presenterClass_javaxInjection_jakartaGeneration() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestPresenter.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.presenter.Presenter
          import androidx.compose.runtime.Composable
          import javax.inject.Inject

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          class FavoritesPresenter @Inject constructor(val value: String): Presenter<FavoritesScreen.State> {
            @Composable
            override fun present(): FavoritesScreen.State {
              throw NotImplementedError()
            }
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesPresenterFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.Navigator
        import com.slack.circuit.runtime.presenter.Presenter
        import com.slack.circuit.runtime.screen.Screen
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject
        import jakarta.inject.Provider

        @ContributesMultibinding(AppScope::class)
        public class FavoritesPresenterFactory @Inject constructor(
          private val provider: Provider<FavoritesPresenter>,
        ) : Presenter.Factory {
          override fun create(
            screen: Screen,
            navigator: Navigator,
            context: CircuitContext,
          ): Presenter<*>? = when (screen) {
            is FavoritesScreen -> provider.get()
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun presenterClass_simpleInjection() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestPresenter.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.presenter.Presenter
          import androidx.compose.runtime.Composable
          import jakarta.inject.Inject

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          class FavoritesPresenter @Inject constructor() : Presenter<FavoritesScreen.State> {
            @Composable
            override fun present(): FavoritesScreen.State {
              throw NotImplementedError()
            }
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesPresenterFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.Navigator
        import com.slack.circuit.runtime.presenter.Presenter
        import com.slack.circuit.runtime.screen.Screen
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject
        import jakarta.inject.Provider

        @ContributesMultibinding(AppScope::class)
        public class FavoritesPresenterFactory @Inject constructor(
          private val provider: Provider<FavoritesPresenter>,
        ) : Presenter.Factory {
          override fun create(
            screen: Screen,
            navigator: Navigator,
            context: CircuitContext,
          ): Presenter<*>? = when (screen) {
            is FavoritesScreen -> provider.get()
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun presenterClass_simpleInjection_kotlinInjectAnvil() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestPresenter.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.presenter.Presenter
          import androidx.compose.runtime.Composable
          import me.tatarka.inject.annotations.Inject

          @Inject
          @CircuitInject(FavoritesScreen::class, AppScope::class)
          class FavoritesPresenter : Presenter<FavoritesScreen.State> {
            @Composable
            override fun present(): FavoritesScreen.State {
              throw NotImplementedError()
            }
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesPresenterFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.Navigator
        import com.slack.circuit.runtime.presenter.Presenter
        import com.slack.circuit.runtime.screen.Screen
        import me.tatarka.inject.annotations.Inject
        import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding
        import software.amazon.lastmile.kotlin.inject.anvil.`internal`.Origin

        @Inject
        @ContributesBinding(
          AppScope::class,
          multibinding = true,
        )
        @Origin(FavoritesPresenter::class)
        public class FavoritesPresenterFactory(
          private val provider: () -> FavoritesPresenter,
        ) : Presenter.Factory {
          override fun create(
            screen: Screen,
            navigator: Navigator,
            context: CircuitContext,
          ): Presenter<*>? = when (screen) {
            is FavoritesScreen -> provider()
            else -> null
          }
        }
        """
          .trimIndent(),
      codegenMode = CodegenMode.KOTLIN_INJECT_ANVIL,
    )
  }

  @Test
  fun presenterClass_assistedInjection_kotlinInjectAnvil() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestPresenter.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.presenter.Presenter
          import androidx.compose.runtime.Composable
          import me.tatarka.inject.annotations.Assisted
          import me.tatarka.inject.annotations.Inject

          @Inject
          @CircuitInject(FavoritesScreen::class, AppScope::class)
          class FavoritesPresenter(
            private val value: String,
            @Assisted private val screen: FavoritesScreen,
          ) : Presenter<FavoritesScreen.State> {
            @Composable
            override fun present(): FavoritesScreen.State {
              throw NotImplementedError()
            }
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesPresenterFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.Navigator
        import com.slack.circuit.runtime.presenter.Presenter
        import com.slack.circuit.runtime.screen.Screen
        import me.tatarka.inject.annotations.Inject
        import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding
        import software.amazon.lastmile.kotlin.inject.anvil.`internal`.Origin

        @Inject
        @ContributesBinding(
          AppScope::class,
          multibinding = true,
        )
        @Origin(FavoritesPresenter::class)
        public class FavoritesPresenterFactory(
          private val factory: (screen: FavoritesScreen) -> FavoritesPresenter,
        ) : Presenter.Factory {
          override fun create(
            screen: Screen,
            navigator: Navigator,
            context: CircuitContext,
          ): Presenter<*>? = when (screen) {
            is FavoritesScreen -> factory(screen)
            else -> null
          }
        }
        """
          .trimIndent(),
      codegenMode = CodegenMode.KOTLIN_INJECT_ANVIL,
    )
  }

  @Test
  fun presenterClass_assistedInjection() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestPresenter.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.Navigator
          import com.slack.circuit.runtime.presenter.Presenter
          import androidx.compose.runtime.Composable
          import dagger.assisted.Assisted
          import dagger.assisted.AssistedFactory
          import dagger.assisted.AssistedInject

          class FavoritesPresenter @AssistedInject constructor(
            @Assisted private val screen: FavoritesScreen,
            @Assisted private val navigator: Navigator,
          ) : Presenter<FavoritesScreen.State> {
            @CircuitInject(FavoritesScreen::class, AppScope::class)
            @AssistedFactory
            fun interface Factory {
              fun create(screen: FavoritesScreen, navigator: Navigator): FavoritesPresenter
            }

            @Composable
            override fun present(): FavoritesScreen.State {
              throw NotImplementedError()
            }
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesPresenterFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.Navigator
        import com.slack.circuit.runtime.presenter.Presenter
        import com.slack.circuit.runtime.screen.Screen
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class FavoritesPresenterFactory @Inject constructor(
          private val factory: FavoritesPresenter.Factory,
        ) : Presenter.Factory {
          override fun create(
            screen: Screen,
            navigator: Navigator,
            context: CircuitContext,
          ): Presenter<*>? = when (screen) {
            is FavoritesScreen -> factory.create(screen = screen, navigator = navigator)
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun presenterClass_assistedInjectionWithCircuitContext() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "TestPresenter.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.CircuitContext
          import com.slack.circuit.runtime.Navigator
          import com.slack.circuit.runtime.presenter.Presenter
          import androidx.compose.runtime.Composable
          import dagger.assisted.Assisted
          import dagger.assisted.AssistedFactory
          import dagger.assisted.AssistedInject

          class FavoritesPresenter @AssistedInject constructor(
            @Assisted private val screen: FavoritesScreen,
            @Assisted private val navigator: Navigator,
            @Assisted private val circuitContext: CircuitContext
          ) : Presenter<FavoritesScreen.State> {
            @CircuitInject(FavoritesScreen::class, AppScope::class)
            @AssistedFactory
            fun interface Factory {
              fun create(screen: FavoritesScreen, navigator: Navigator, circuitContext: CircuitContext): FavoritesPresenter
            }

            @Composable
            override fun present(): FavoritesScreen.State {
              throw NotImplementedError()
            }
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesPresenterFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.Navigator
        import com.slack.circuit.runtime.presenter.Presenter
        import com.slack.circuit.runtime.screen.Screen
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class FavoritesPresenterFactory @Inject constructor(
          private val factory: FavoritesPresenter.Factory,
        ) : Presenter.Factory {
          override fun create(
            screen: Screen,
            navigator: Navigator,
            context: CircuitContext,
          ): Presenter<*>? = when (screen) {
            is FavoritesScreen -> factory.create(screen = screen, navigator = navigator, circuitContext = context)
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun hiltCodegenMode_skipsAnvilBinding() {
    assertGeneratedFile(
      codegenMode = CodegenMode.HILT,
      sourceFile =
        kotlin(
          "TestPresenterWithoutAnvil.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.Navigator
          import com.slack.circuit.runtime.presenter.Presenter
          import androidx.compose.runtime.Composable
          import dagger.assisted.Assisted
          import dagger.assisted.AssistedFactory
          import dagger.assisted.AssistedInject
          import dagger.hilt.components.SingletonComponent

          class FavoritesPresenter @AssistedInject constructor(
            @Assisted private val screen: FavoritesScreen,
            @Assisted private val navigator: Navigator,
          ) : Presenter<FavoritesScreen.State> {
            @CircuitInject(FavoritesScreen::class, SingletonComponent::class)
            @AssistedFactory
            fun interface Factory {
              fun create(screen: FavoritesScreen, navigator: Navigator): FavoritesPresenter
            }

            @Composable
            override fun present(): FavoritesScreen.State {
              throw NotImplementedError()
            }
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesPresenterFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.Navigator
        import com.slack.circuit.runtime.presenter.Presenter
        import com.slack.circuit.runtime.screen.Screen
        import dagger.hilt.codegen.OriginatingElement
        import jakarta.inject.Inject

        @OriginatingElement(topLevelClass = FavoritesPresenter::class)
        public class FavoritesPresenterFactory @Inject constructor(
          private val factory: FavoritesPresenter.Factory,
        ) : Presenter.Factory {
          override fun create(
            screen: Screen,
            navigator: Navigator,
            context: CircuitContext,
          ): Presenter<*>? = when (screen) {
            is FavoritesScreen -> factory.create(screen = screen, navigator = navigator)
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun hiltCodegenMode_module() {
    assertGeneratedFile(
      codegenMode = CodegenMode.HILT,
      sourceFile =
        kotlin(
          "TestPresenterWithoutAnvil.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.Navigator
          import com.slack.circuit.runtime.presenter.Presenter
          import androidx.compose.runtime.Composable
          import dagger.assisted.Assisted
          import dagger.assisted.AssistedFactory
          import dagger.assisted.AssistedInject
          import dagger.hilt.components.SingletonComponent

          class FavoritesPresenter @AssistedInject constructor(
            @Assisted private val screen: FavoritesScreen,
            @Assisted private val navigator: Navigator,
          ) : Presenter<FavoritesScreen.State> {
            @CircuitInject(FavoritesScreen::class, SingletonComponent::class)
            @AssistedFactory
            fun interface Factory {
              fun create(screen: FavoritesScreen, navigator: Navigator): FavoritesPresenter
            }

            @Composable
            override fun present(): FavoritesScreen.State {
              throw NotImplementedError()
            }
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesPresenterFactoryModule.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.presenter.Presenter
        import dagger.Binds
        import dagger.Module
        import dagger.hilt.InstallIn
        import dagger.hilt.codegen.OriginatingElement
        import dagger.hilt.components.SingletonComponent
        import dagger.multibindings.IntoSet

        @Module
        @InstallIn(SingletonComponent::class)
        @OriginatingElement(topLevelClass = FavoritesPresenter::class)
        public abstract class FavoritesPresenterFactoryModule {
          @Binds
          @IntoSet
          public abstract fun bindFavoritesPresenterFactory(favoritesPresenterFactory: FavoritesPresenterFactory): Presenter.Factory
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun hiltCodegenMode_topLevelOriginatingElement() {
    assertGeneratedFile(
      codegenMode = CodegenMode.HILT,
      sourceFile =
        kotlin(
          "Home.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier
          import dagger.hilt.components.SingletonComponent

          @CircuitInject(HomeScreen::class, SingletonComponent::class)
          @Composable
          fun Home(modifier: Modifier = Modifier) {

          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/HomeFactoryModule.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.ui.Ui
        import dagger.Binds
        import dagger.Module
        import dagger.hilt.InstallIn
        import dagger.hilt.components.SingletonComponent
        import dagger.multibindings.IntoSet

        @Module
        @InstallIn(SingletonComponent::class)
        public abstract class HomeFactoryModule {
          @Binds
          @IntoSet
          public abstract fun bindHomeFactory(homeFactory: HomeFactory): Ui.Factory
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun invalidSupertypes() {
    assertProcessingError(
      sourceFile =
        kotlin(
          "InvalidSupertypes.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          class Favorites {
            @Composable
            fun Content(state: FavoritesScreen.State, modifier: Modifier) {

            }
          }
          """
            .trimIndent(),
        )
    ) { messages ->
      assertThat(messages)
        .contains("Factory must be for a UI or Presenter class, but was test.Favorites.")
    }
  }

  @Test
  fun presenterFunctionMissingReturn() {
    assertProcessingError(
      sourceFile =
        kotlin(
          "MissingPresenterReturn.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          @Composable
          fun FavoritesPresenter() {

          }
          """
            .trimIndent(),
        )
    ) { messages ->
      assertThat(messages)
        .contains("MissingPresenterReturn.kt:8: Presenter functions must return a UiState.")
    }
  }

  @Test
  fun uiFunctionMissingModifier() {
    assertProcessingError(
      sourceFile =
        kotlin(
          "MissingModifierParam.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          @Composable
          fun Favorites() {

          }
          """
            .trimIndent(),
        )
    ) { messages ->
      assertThat(messages).contains("UI composable functions must have a Modifier parameter!")
    }
  }

  @Test
  fun invalidAssistedInjection() {
    assertProcessingError(
      sourceFile =
        kotlin(
          "InvalidAssistedInjection.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable
          import dagger.assisted.Assisted
          import dagger.assisted.AssistedInject
          import dagger.assisted.AssistedFactory

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          class Favorites @AssistedInject constructor(@Assisted input: String) : Presenter<FavoritesScreen.State> {
            @Composable
            override fun present(): FavoritesScreen.State {

            }

            @AssistedFactory
            fun interface Factory {
              fun create(input: String): Favorites
            }
          }
          """
            .trimIndent(),
        )
    ) { messages ->
      assertThat(messages)
        .contains(
          "When using @CircuitInject with an @AssistedInject-annotated class, you must put " +
            "the @CircuitInject annotation on the @AssistedFactory-annotated nested" +
            " class (test.Favorites.Factory)."
        )
    }
  }

  @Test
  fun invalidAssistedInjection_missingFactory() {
    assertProcessingError(
      sourceFile =
        kotlin(
          "InvalidAssistedInjection.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import androidx.compose.runtime.Composable
          import dagger.assisted.Assisted
          import dagger.assisted.AssistedInject

          @CircuitInject(FavoritesScreen::class, AppScope::class)
          class Favorites @AssistedInject constructor(@Assisted input: String) : Presenter<FavoritesScreen.State> {
            @Composable
            override fun present(): FavoritesScreen.State {

            }
          }
          """
            .trimIndent(),
        )
    ) { messages ->
      assertThat(messages)
        .contains(
          "When using @CircuitInject with an @AssistedInject-annotated class, you must put " +
            "the @CircuitInject annotation on the @AssistedFactory-annotated nested class."
        )
    }
  }

  // Regression test for https://github.com/slackhq/circuit/issues/1704
  @Test
  fun parameterOrderDoesNotMatter() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "ParameterOrdering.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.screen.StaticScreen
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier

          data object Static : StaticScreen

          @CircuitInject(Static::class, AppScope::class)
          @Composable
          fun StaticUi(screen: Static, modifier: Modifier) {}
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/StaticUiFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.CircuitUiState
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.slack.circuit.runtime.ui.ui
        import com.squareup.anvil.annotations.ContributesMultibinding
        import jakarta.inject.Inject

        @ContributesMultibinding(AppScope::class)
        public class StaticUiFactory @Inject constructor() : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            Static -> ui<CircuitUiState> { _, modifier -> StaticUi(modifier = modifier, screen = screen as Static) }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun parameterOrderDoesNotMatter_StaticScreen_kotlinInjectAnvil() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "ParameterOrdering.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.screen.StaticScreen
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier

          data object Static : StaticScreen

          @CircuitInject(Static::class, AppScope::class)
          @Composable
          fun StaticUi(screen: Static, modifier: Modifier) {}
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/StaticUiFactory.kt",
      codegenMode = CodegenMode.KOTLIN_INJECT_ANVIL,
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.CircuitUiState
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.slack.circuit.runtime.ui.ui
        import me.tatarka.inject.annotations.Inject
        import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding

        @Inject
        @ContributesBinding(
          AppScope::class,
          multibinding = true,
        )
        public class StaticUiFactory : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            Static -> ui<CircuitUiState> { _, modifier -> StaticUi(modifier = modifier, screen = screen as Static) }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun parameterOrderDoesNotMatter_NormalScreen_kotlinInjectAnvil() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "ParameterOrdering.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.screen.Screen
          import androidx.compose.runtime.Composable
          import androidx.compose.ui.Modifier

          data class TestScreen(val value: String) : Screen

          @CircuitInject(TestScreen::class, AppScope::class)
          @Composable
          fun TestUi(screen: TestScreen, modifier: Modifier) {}
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/TestUiFactory.kt",
      codegenMode = CodegenMode.KOTLIN_INJECT_ANVIL,
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.CircuitUiState
        import com.slack.circuit.runtime.screen.Screen
        import com.slack.circuit.runtime.ui.Ui
        import com.slack.circuit.runtime.ui.ui
        import me.tatarka.inject.annotations.Inject
        import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding

        @Inject
        @ContributesBinding(
          AppScope::class,
          multibinding = true,
        )
        public class TestUiFactory : Ui.Factory {
          override fun create(screen: Screen, context: CircuitContext): Ui<*>? = when (screen) {
            is TestScreen -> ui<CircuitUiState> { _, modifier -> TestUi(modifier = modifier, screen = screen) }
            else -> null
          }
        }
        """
          .trimIndent(),
    )
  }

  @Test
  fun presenterClass_simpleInjection_metro() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "FavoritesPresenter.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.presenter.Presenter
          import androidx.compose.runtime.Composable
          import dev.zacsweers.metro.Inject

          @Inject
          @CircuitInject(FavoritesScreen::class, AppScope::class)
          class FavoritesPresenter : Presenter<FavoritesScreen.State> {
            @Composable
            override fun present(): FavoritesScreen.State {
              throw NotImplementedError()
            }
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesPresenterFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.Navigator
        import com.slack.circuit.runtime.presenter.Presenter
        import com.slack.circuit.runtime.screen.Screen
        import dev.zacsweers.metro.ContributesIntoSet
        import dev.zacsweers.metro.Inject
        import dev.zacsweers.metro.Origin
        import dev.zacsweers.metro.Provider

        @Inject
        @ContributesIntoSet(AppScope::class)
        @Origin(FavoritesPresenter::class)
        public class FavoritesPresenterFactory(
          private val provider: Provider<FavoritesPresenter>,
        ) : Presenter.Factory {
          override fun create(
            screen: Screen,
            navigator: Navigator,
            context: CircuitContext,
          ): Presenter<*>? = when (screen) {
            is FavoritesScreen -> provider()
            else -> null
          }
        }
        """
          .trimIndent(),
      codegenMode = CodegenMode.METRO,
    )
  }

  @Test
  fun presenterClass_assistedInjection_metro() {
    assertGeneratedFile(
      sourceFile =
        kotlin(
          "FavoritesPresenter.kt",
          """
          package test

          import com.slack.circuit.codegen.annotations.CircuitInject
          import com.slack.circuit.runtime.Navigator
          import com.slack.circuit.runtime.presenter.Presenter
          import androidx.compose.runtime.Composable
          import dev.zacsweers.metro.Assisted
          import dev.zacsweers.metro.AppScope
          import dev.zacsweers.metro.AssistedFactory
          import dev.zacsweers.metro.Inject

          @Inject
          class FavoritesPresenter(
            @Assisted private val navigator: Navigator
          ) : Presenter<FavoritesScreen.State> {
            @CircuitInject(FavoritesScreen::class, AppScope::class)
            @AssistedFactory
            fun interface Factory {
              fun create(@Assisted navigator: Navigator): FavoritesPresenter
            }

            @Composable
            override fun present(): FavoritesScreen.State {
              throw NotImplementedError()
            }
          }
          """
            .trimIndent(),
        ),
      generatedFilePath = "test/FavoritesPresenterFactory.kt",
      expectedContent =
        """
        package test

        import com.slack.circuit.runtime.CircuitContext
        import com.slack.circuit.runtime.Navigator
        import com.slack.circuit.runtime.presenter.Presenter
        import com.slack.circuit.runtime.screen.Screen
        import dev.zacsweers.metro.AppScope
        import dev.zacsweers.metro.ContributesIntoSet
        import dev.zacsweers.metro.Inject
        import dev.zacsweers.metro.Origin

        @Inject
        @ContributesIntoSet(AppScope::class)
        @Origin(FavoritesPresenter::class)
        public class FavoritesPresenterFactory(
          private val factory: FavoritesPresenter.Factory,
        ) : Presenter.Factory {
          override fun create(
            screen: Screen,
            navigator: Navigator,
            context: CircuitContext,
          ): Presenter<*>? = when (screen) {
            is FavoritesScreen -> factory.create(navigator = navigator)
            else -> null
          }
        }
        """
          .trimIndent(),
      codegenMode = CodegenMode.METRO,
    )
  }

  private fun assertGeneratedFile(
    sourceFile: SourceFile,
    generatedFilePath: String,
    @Language("kotlin") expectedContent: String,
    codegenMode: CodegenMode = CodegenMode.ANVIL,
    kspOptions: Map<String, String> = emptyMap(),
  ) {
    val compilation =
      prepareCompilation(sourceFile, codegenMode = codegenMode, kspOptions = kspOptions)
    val result = compilation.compile()
    assertThat(result.exitCode).isEqualTo(ExitCode.OK)
    val generatedSourcesDir = compilation.kspSourcesDir
    val generatedAdapter = File(generatedSourcesDir, "kotlin/$generatedFilePath")
    if (!generatedAdapter.exists()) {
      throw AssertionError("No adapter found at path $generatedFilePath\n${result.messages}")
    }
    assertThat(generatedAdapter.exists()).isTrue()
    assertThat(generatedAdapter.readText().trim()).isEqualTo(expectedContent.trimIndent())
  }

  private fun assertProcessingError(
    sourceFile: SourceFile,
    codegenMode: CodegenMode = CodegenMode.ANVIL,
    kspOptions: Map<String, String> = emptyMap(),
    body: (messages: String) -> Unit,
  ) {
    val compilation =
      prepareCompilation(sourceFile, codegenMode = codegenMode, kspOptions = kspOptions)
    val result = compilation.compile()
    assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
    body(result.messages)
  }

  private fun prepareCompilation(
    vararg sourceFiles: SourceFile,
    codegenMode: CodegenMode,
    kspOptions: Map<String, String>,
  ): KotlinCompilation =
    KotlinCompilation().apply {
      jvmTarget = "11"
      sources =
        sourceFiles.toList() +
          screens +
          circuitSymbols +
          when (codegenMode) {
            CodegenMode.UNKNOWN -> error("Not possible in tests")
            CodegenMode.ANVIL -> listOf(appScope, anvilAnnotations)
            CodegenMode.HILT -> hiltSymbols
            CodegenMode.KOTLIN_INJECT_ANVIL -> {
              kotlinInjectSymbols + appScope
            }
            CodegenMode.METRO -> listOf(appScope, metroSymbols)
          }
      inheritClassPath = true
      kotlincArguments += "-Xannotation-default-target=param-property"
      configureKsp {
        kspProcessorOptions += CircuitOptions.MODE to codegenMode.name
        kspProcessorOptions += kspOptions
        symbolProcessorProviders += CircuitSymbolProcessorProvider()
      }
    }
}
